'''
Created on 30 Dec 2009

@author: ai
'''

import os
import sys
import re
from pprint import pprint
import datetime
import logging
import sqlite3
import cPickle as pickle
import base64

# ------------------------------------------------------------------------------
#    My modules.
# ------------------------------------------------------------------------------
from AVD import AVD
from SCM import SCMRevision
# ------------------------------------------------------------------------------

class TestRun(object):
    """ Convenience wrapper around a single test run, e.g. when it started,
    when it stopped, did it pass, any exception stacks, etc.
    """   
    
    def __init__(self, name, parent_class, package, start, stop, is_pass, output=None):
        self.name = name
        self.parent_class = parent_class
        self.package = package
        self.fullname = "%s.%s.%s" % (self.package, self.parent_class, self.name)
                    
        self.start = start
        self.stop = stop
        assert(self.stop >= self.start)
        self.is_pass = is_pass
        self.output = output        

    def __repr__(self):
        output =  "\nFull name: %s" % (self.fullname, )        
        output += "\nStart: %s" % (self.start, )
        output += "\nStop: %s" % (self.stop, )
        output += "\nIs Pass: %s" % (self.is_pass, )
        output += "\nOutput: %s" % (self.output, )
        return output
    
    def __cmp__(self, other):
        """ Two TestRun objects are identical if they share the same full name
        and were executed at the same time.  This means two TestRuns for the
        same test function but run at different times are not the same.
        """
        if all(getattr(self, elem) == getattr(other, elem) for elem in ["fullname", "start", "stop"]):
            return 0
        return -1 
    
    @staticmethod
    def convert_to_str(obj):
        return base64.b64encode((pickle.dumps(obj, -1)))
    
    @staticmethod
    def convert_from_str(s):
        return pickle.loads(base64.b64decode(s))        
    
DB_NAME = "results.db"

class Database(object):
    """ Wrapper around a sqlite database for the ResultsManager.
    """        
    
    def __init__(self, results_dir, db_name=DB_NAME):
        self.db = sqlite3.connect(os.path.join(results_dir, db_name), detect_types=sqlite3.PARSE_DECLTYPES)        
        self.initialise_tables()
        
    def initialise_tables(self, delete_first=False):
        """ Create the initial set of tables.  If delete_first is True then
        delete the tables before creating.  
        """
        if delete_first:
            self.db.execute("DROP TABLE IF EXISTS results")            
        
        self.db.execute(("CREATE TABLE IF NOT EXISTS avds("
                         "name TEXT PRIMARY KEY, "
                         "avd BLOB NOT NULL)"))
        
        self.db.execute(("CREATE TABLE IF NOT EXISTS scm_revisions("
                         "id TEXT PRIMARY KEY, "
                         "scm_revision BLOB NOT NULL)"))
        
        self.db.execute(("CREATE TABLE IF NOT EXISTS testruns("
                         "id INTEGER PRIMARY KEY, "
                         "testrun BLOB NOT NULL)"))
        
        self.db.execute(("CREATE TABLE IF NOT EXISTS results("
                         "id INTEGER PRIMARY KEY, "
                         "scm_revision_id TEXT NOT NULL, "
                         "avd_name TEXT NOT NULL, "
                         "testrun_id INTEGER NOT NULL)"))
        self.db.commit()
        
    def close(self):        
        self.db.close()
        
    def insert(self, avd, scm_revision, testrun):
        """ Insert a row into the results table.  This will implicitly insert
        an AVD into the avds table, SCMRevision in the scm_revisions table, and
        a TestRun into the testruns table, if they do not already exist inside
        them.
        """        
        self.insert_avd(avd)
        self.insert_scm_revision(scm_revision)
        self.insert_testrun(testrun)        
        self.insert_result(avd, scm_revision, testrun)
        self.db.commit()       
        
    def insert_avd(self, avd):
        pass
    
    def insert_scm_revision(self, scm_revision):
        pass
    
    def insert_testrun(self, testrun):
        pass
    
    def get_avds(self):
        pass
        
    def get_scm_revisions(self):
        pass
    
    def get_testruns

class ResultsManager(object):
    """ Deals with CRUD operations around the results.
    """  
        
    # --------------------------------------------------------------------------
    #    ResultsManager regular expressions.
    # --------------------------------------------------------------------------    
    # e.g. 12-29 20:16:50.305 I/TestRunner(  255): started: test2(com.project.simple.tests.SimpleActivityBasicTest)  
    RE_LOGCAT_TEST_START = re.compile(("started:"
                                       "\s+(?P<test_name>\S+)"
                                       "\((?P<package_and_class>\S+)\)"))
    
    # e.g. 12-29 20:16:51.215 I/TestRunner(  255): finished: test2(com.project.simple.tests.SimpleActivityBasicTest)
    RE_LOGCAT_TEST_FINISH = re.compile(("finished:"
                                        "\s+(?P<test_name>\S+)"
                                        "\((?P<package_and_class>\S+)\)"))    
    
    # e.g. 12-29 20:16:51.225 I/TestRunner(  255): passed: test2(com.project.simple.tests.SimpleActivityBasicTest)
    RE_LOGCAT_TEST_PASS = re.compile(("passed:"
                                      "\s+(?P<test_name>\S+)"
                                      "\((?P<package_and_class>\S+)\)"))    
    
    # e.g. 12-29 20:16:52.915 I/TestRunner(  255): failed: testText(com.project.simple.tests.SimpleActivityCheckTextTest)
    RE_LOGCAT_TEST_FAIL = re.compile(("failed:"
                                      "\s+(?P<test_name>\S+)"
                                      "\((?P<package_and_class>\S+)\)"))
    
    # contents of a TestRunner logcat line (i.e. with the date parsed and
    # excluding all the preceding tokens about process name, log level, etc.)
    RE_LOGCAT_LINE = re.compile(("^(?P<month>\d+)[^\d]+"
                                 "(?P<day>\d+)[^\d]+"
                                 "(?P<hour>\d+)[^\d]+"
                                 "(?P<minute>\d+)[^\d]+"
                                 "(?P<second>\d+)[^\d]+"
                                 "(?P<millisecond>\d+).*"
                                 "TestRunner.*"
                                 "\): (?P<content>.*)"))
    # --------------------------------------------------------------------------
        
    def __init__(self, scm_handler, results_dir):
        self.scm_handler = scm_handler
        self.results_dir = results_dir
        assert(os.path.isdir(self.results_dir))
        self.db = Database(self.results_dir)
        
    def close(self):
        self.db.close()
        
    def __del__(self):
        self.close()
        
    def convert_logcat_file_to_testruns(self, logcat_file):
        """ Take a file containing the 'adb logcat -v' output of test runs
        and convert it to a list of TestRun objects.  Returns that list of
        TestRun objects, or False if something went wrong."""
        logger = logging.getLogger("convert_logcat_file_to_testruns")
        assert(os.path.isfile(logcat_file))
                
        # the output from 'adb logcat -v' doesn't include any information
        # about what year the tests are executed in.  since the output from a
        # logcat session is intended to be temporary (it will be deleted
        # along with the rest of the temporary directory) it's safe to assume
        # the current year is correct and detect for the month wrapping around
        # in case tests are executed over the New Year.
        year = datetime.datetime.now().year
        month = datetime.datetime.now().month
             
        lines = []
        with open(logcat_file) as f:
            for line in f:
                m = self.RE_LOGCAT_LINE.match(line)
                if m is None:
                    continue
                input_month = int(m.group('month'))
                if input_month < month:                    
                    year += 1          
                month = input_month          
                date = datetime.datetime(year, month, int(m.group('day')), int(m.group('hour')), int(m.group('minute')), int(m.group('second')), int(m.group('millisecond')))
                lines.append((date, m.group('content')))                
        
        testruns = []
        name, start, stop, package_and_class, parent_class, package = (None, None, None, None, None, None)
        started, finished, passed, failed = (False, False, False, False)
        output = ""
        for (date, line) in lines:
            m = self.RE_LOGCAT_TEST_START.match(line)
            if m:
                if started and finished:
                    # previous test run encountered, so create a TestRun object
                    # for it.
                    assert((passed and not failed) or (not passed and failed))                 
                    tr = TestRun(name, parent_class, package, start, stop, passed, output)                
                    testruns.append(tr)
                name, start, stop, package_and_class, parent_class, package = (None, None, None, None, None, None)            
                started, finished, passed, failed = (False, False, False, False)
                output = ""
                start = date
                name = m.group('test_name')
                package_and_class = m.group('package_and_class')
                package = '.'.join(package_and_class.split('.')[:-1])
                parent_class = package_and_class.split('.')[-1]
                started = True
                continue            
            m = self.RE_LOGCAT_TEST_FINISH.match(line)
            if m:
                stop = date
                assert(name is not None)
                assert(name == m.group('test_name'))
                assert(package_and_class is not None)
                assert(package_and_class == m.group('package_and_class'))
                finished = True                                
                continue
            m = self.RE_LOGCAT_TEST_PASS.match(line)
            if m:
                assert(name is not None)
                assert(name == m.group('test_name'))
                assert(package_and_class is not None)
                assert(package_and_class == m.group('package_and_class'))
                passed = True
                continue
            m = self.RE_LOGCAT_TEST_FAIL.match(line)
            if m:
                assert(name is not None)
                assert(name == m.group('test_name'))
                assert(package_and_class is not None)
                assert(package_and_class == m.group('package_and_class'))                
                failed = True                
                continue
            
            # at this point we would have hit a 'continue' if the line was
            # regarding starting, finishing, passing, or failing.  hence this
            # must be some output, e.g. an exception stack in the case of a
            # failure.
            output += "%s\n" % (line, )
        
        if started and finished:
            # one more trailing test run to add.
            assert((passed and not failed) or (not passed and failed))                        
            tr = TestRun(name, parent_class, package, start, stop, passed, output)                
            testruns.append(tr)
                            
        return testruns
    
if __name__ == "__main__":
    rm = ResultsManager(None, r"I:\eclipse_workspace\sisyphus_results")
    testruns = rm.convert_logcat_file_to_testruns(r"I:\temp\tmpgs5rpd\emulator-5554_logcat.txt")
    pprint(testruns)

    
    